/********************************************************************************
 * Copyright (c) 2011-2017 Red Hat Inc. and/or its affiliates and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 1.0 which is available at
 * http://www.eclipse.org/legal/epl-v10.html.
 *
 * SPDX-License-Identifier: EPL-1.0
 ********************************************************************************/
package org.eclipse.ceylon.ide.eclipse.code.complete;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.TextPresentation;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationPresenter;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;

import org.eclipse.ceylon.ide.eclipse.code.editor.CeylonEditor;
import org.eclipse.ceylon.ide.eclipse.code.editor.CeylonSourceViewer;

class ParameterContextValidator 
        implements IContextInformationValidator, IContextInformationPresenter {
    
    private int position;
    private IContextInformation information;
    private int currentParameter;
    private CeylonEditor editor;
    
    ParameterContextValidator(CeylonEditor editor) {
        this.editor = editor;
    }

    @Override
    public boolean updatePresentation(int brokenPosition, 
            TextPresentation presentation) {
        
        String s = information.getInformationDisplayString();
        presentation.clear();
        
        if (this.position==-1) {
            presentation.addStyleRange(new StyleRange(0, s.length(), 
                    null, null, SWT.BOLD));
            addItalics(presentation, s);
            return true;
        }
        
        int currentParameter = -1;
        CeylonSourceViewer viewer = editor.getCeylonSourceViewer();
        int position = viewer.getSelectedRange().x;
        IDocument doc = viewer.getDocument();
        try {
            boolean namedInvocation = doc.getChar(this.position)=='{';
            if (!namedInvocation) Assert.isTrue(doc.getChar(this.position)=='(');
//            int paren = doc.get(this.position, position-this.position)
//                    .indexOf(namedInvocation?'{':'(');
//            if (paren<0) { //TODO: is this really useful?
//                this.position = doc.get(0, position).lastIndexOf('(');
//            }
            currentParameter = getCharCount(doc, 
                    this.position+1, position, 
                    namedInvocation?";":",", "", true);
        } 
        catch (BadLocationException x) {
            return false;
        }

        if (currentParameter != -1) {
            if (this.currentParameter == currentParameter) {
                return false;
            }
        }

        presentation.clear();
        this.currentParameter = currentParameter;

        int[] commas = computeCommaPositions(s);

        if (commas.length - 2 < currentParameter) {
            presentation.addStyleRange(new StyleRange(0, s.length(), 
                    null, null, SWT.NORMAL));
            addItalics(presentation, s);
            return true;
        }

        int start = commas[currentParameter] + 1;
        int end = commas[currentParameter + 1];
        if (start > 0) {
            presentation.addStyleRange(new StyleRange(0, start, 
                    null, null, SWT.NORMAL));
        }
        if (end > start) {
            presentation.addStyleRange(new StyleRange(start, end - start, 
                    null, null, SWT.BOLD));
        }
        if (end < s.length()) {
            presentation.addStyleRange(new StyleRange(end, s.length() - end, 
                    null, null, SWT.NORMAL));
        }
        
        addItalics(presentation, s);
        
        return true;
    }

    private void addItalics(TextPresentation presentation, String s) {
        Matcher m2 = p2.matcher(s);
        while (m2.find()) {
            presentation.mergeStyleRange(new StyleRange(m2.start(), m2.end()-m2.start(), 
                    null, null, SWT.ITALIC));
        }
//      Matcher m1 = p1.matcher(s);
//      while (m1.find()) {
//          presentation.mergeStyleRange(new StyleRange(m1.start(), m1.end()-m1.start()+1, 
//                  typeColor, null));
//      }
    }

//    final Pattern p1 = Pattern.compile("\\b\\p{javaUpperCase}\\w*\\b");
    final Pattern p2 = Pattern.compile("\\b\\p{javaLowerCase}\\w*\\b");
//    final Color typeColor = color(getCurrentTheme().getColorRegistry(), TYPES);

    @Override
    public void install(IContextInformation info, ITextViewer viewer, 
            int documentPosition) {
        if (info instanceof ParameterContextInformation) {
        	ParameterContextInformation pci = 
                    (ParameterContextInformation) info;
            this.position = pci.getArgumentListOffset();
        }
        else if (info instanceof ParametersCompletionProposal.ParameterContextInformation) {
        	ParametersCompletionProposal.ParameterContextInformation pci = 
                    (ParametersCompletionProposal.ParameterContextInformation) info;
            this.position = pci.getArgumentListOffset();
        }
        else {
            this.position = -1;
        }
        Assert.isTrue(viewer==editor.getCeylonSourceViewer());
        this.information = info;
        this.currentParameter= -1;
    }
    
    @Override
    public boolean isContextInformationValid(int brokenPosition) {
        if (editor.isInLinkedMode()) {
            Object linkedModeOwner = editor.getLinkedModeOwner();
            if (linkedModeOwner instanceof InvocationCompletionProposal ||
                linkedModeOwner instanceof ParametersCompletionProposal ||
                linkedModeOwner instanceof org.eclipse.ceylon.ide.common.completion.InvocationCompletionProposal ||
                linkedModeOwner instanceof org.eclipse.ceylon.ide.common.completion.RefinementCompletionProposal) {
                return true;
            }
        }
        try {
            CeylonSourceViewer viewer = editor.getCeylonSourceViewer();
            int position = viewer.getSelectedRange().x;
            if (position < this.position) {
                return false;
            }
            
            IDocument document = viewer.getDocument();
            IRegion line = 
                    document.getLineInformationOfOffset(this.position);
            
            if (position < line.getOffset() || 
                    position >= document.getLength()) {
                return false;
            }
//            System.out.println(document.get(this.position, position-this.position));
            int semiCount = getCharCount(document, this.position, position, ";", "", true);
            int fenceCount = getCharCount(document, this.position, position, "{(", "})", false);
            return semiCount==0 && fenceCount>0;

        } 
        catch (BadLocationException x) {
            return false;
        }
    }
    
    /*@Override
    public boolean isContextInformationValid(int offset) {
        IContextInformation[] infos= computeContextInformation(viewer, offset);
        if (infos != null && infos.length > 0) {
            for (int i= 0; i < infos.length; i++)
                if (information.equals(infos[i]))
                    return true;
        }
        return false;
    }*/
    
    private static final int NONE = 0;
    private static final int BRACKET = 1;
    private static final int BRACE = 2;
    private static final int PAREN = 3;
    private static final int ANGLE = 4;

    private static int getCharCount(IDocument document, 
            final int start, final int end, 
            String increments, String decrements, 
            boolean considerNesting) 
                    throws BadLocationException {

        Assert.isTrue((increments.length() != 0 || decrements.length() != 0) 
                && !increments.equals(decrements));

        int nestingMode = NONE;
        int nestingLevel = 0;

        int charCount = 0;
        int offset = start;
        char prev = ' ';
        while (offset < end) {
            char curr = document.getChar(offset++);
            switch (curr) {
                case '/':
                    if (offset < end) {
                        char next = document.getChar(offset);
                        if (next == '*') {
                            // a comment starts, advance to the comment end
                            offset= getCommentEnd(document, offset + 1, end);
                        }
                        else if (next == '/') {
                            // '//'-comment: nothing to do anymore on this line
                            int nextLine= document.getLineOfOffset(offset) + 1;
                            if (nextLine == document.getNumberOfLines()) {
                                offset= end;
                            }
                            else {
                                offset= document.getLineOffset(nextLine);
                            }
                        }
                    }
                    break;
                case '*':
                    if (offset < end) {
                        char next= document.getChar(offset);
                        if (next == '/') {
                            // we have been in a comment: forget what we read before
                            charCount= 0;
                            ++ offset;
                        }
                    }
                    break;
                case '"':
                case '\'':
                    offset= getStringEnd(document, offset, end, curr);
                    break;
                case '[':
                    if (considerNesting) {
                        if (nestingMode == BRACKET || nestingMode == NONE) {
                            nestingMode= BRACKET;
                            nestingLevel++;
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case ']':
                    if (considerNesting) {
                        if (nestingMode == BRACKET) {
                            if (--nestingLevel == 0) {
                                nestingMode= NONE;
                            }
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case '(':
                    if (considerNesting) {
                        if (nestingMode == ANGLE) {
                            // generics heuristic failed
                            nestingMode=PAREN;
                            nestingLevel= 1;
                        }
                        if (nestingMode == PAREN || nestingMode == NONE) {
                            nestingMode= PAREN;
                            nestingLevel++;
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case ')':
                    if (considerNesting) {
                        if (nestingMode == PAREN) {
                            if (--nestingLevel == 0) {
                                nestingMode= NONE;
                            }
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case '{':
                    if (considerNesting) {
                        if (nestingMode == ANGLE) {
                            // generics heuristic failed
                            nestingMode=BRACE;
                            nestingLevel= 1;
                        }
                        if (nestingMode == BRACE || nestingMode == NONE) {
                            nestingMode= BRACE;
                            nestingLevel++;
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case '}':
                    if (considerNesting) {
                        if (nestingMode == BRACE) {
                            if (--nestingLevel == 0) {
                                nestingMode= NONE;
                            }
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case '<':
                    if (considerNesting) {
                        if (nestingMode == ANGLE || nestingMode == NONE 
                                /*&& checkGenericsHeuristic(document, offset - 1, start - 1)*/) {
                            nestingMode= ANGLE;
                            nestingLevel++;
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case '>':
                    if (considerNesting 
                            && prev != '=') { //check that it's not a fat arrow
                        if (nestingMode == ANGLE) {
                            if (--nestingLevel == 0) {
                                nestingMode= NONE;
                            }
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                default:
                    if (nestingLevel==0) {
                        if (increments.indexOf(curr) >= 0) {
                            ++ charCount;
                        }
                        if (decrements.indexOf(curr) >= 0) {
                            -- charCount;
                        }
                    }
            }
        }

        return charCount;
    }

    static int findCharCount(int count, IDocument document, 
            final int start, final int end, 
            String increments, String decrements, 
            boolean considerNesting) 
                    throws BadLocationException {

        Assert.isTrue((increments.length() != 0 || decrements.length() != 0) 
                && !increments.equals(decrements));

        final int NONE= 0;
        final int BRACKET= 1;
        final int BRACE= 2;
        final int PAREN= 3;
        final int ANGLE= 4;

        int nestingMode= NONE;
        int nestingLevel= 0;

        int charCount= 0;
        int offset= start;
        boolean lastWasEquals = false;
        while (offset < end) {
            if (nestingLevel == 0) {
                if (count==charCount) {
                    return offset-1;
                }
            }
            char curr= document.getChar(offset++);
            switch (curr) {
                case '/':
                    if (offset < end) {
                        char next= document.getChar(offset);
                        if (next == '*') {
                            // a comment starts, advance to the comment end
                            offset= getCommentEnd(document, offset + 1, end);
                        }
                        else if (next == '/') {
                            // '//'-comment: nothing to do anymore on this line
                            int nextLine= document.getLineOfOffset(offset) + 1;
                            if (nextLine == document.getNumberOfLines()) {
                                offset= end;
                            }
                            else {
                                offset= document.getLineOffset(nextLine);
                            }
                        }
                    }
                    break;
                case '*':
                    if (offset < end) {
                        char next= document.getChar(offset);
                        if (next == '/') {
                            // we have been in a comment: forget what we read before
                            charCount= 0;
                            ++ offset;
                        }
                    }
                    break;
                case '"':
                case '\'':
                    offset= getStringEnd(document, offset, end, curr);
                    break;
                case '[':
                    if (considerNesting) {
                        if (nestingMode == BRACKET || nestingMode == NONE) {
                            nestingMode= BRACKET;
                            nestingLevel++;
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case ']':
                    if (considerNesting) {
                        if (nestingMode == BRACKET)
                            if (--nestingLevel == 0) {
                                nestingMode= NONE;
                            }
                        break;
                    }
                    //$FALL-THROUGH$
                case '(':
                    if (considerNesting) {
                        if (nestingMode == ANGLE) {
                            // generics heuristic failed
                            nestingMode=PAREN;
                            nestingLevel= 1;
                        }
                        if (nestingMode == PAREN || nestingMode == NONE) {
                            nestingMode= PAREN;
                            nestingLevel++;
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case ')':
                    if (considerNesting) {
                        if (nestingMode == 0) {
                            return offset-1;
                        }
                        if (nestingMode == PAREN) {
                            if (--nestingLevel == 0) {
                                nestingMode= NONE;
                            }
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case '{':
                    if (considerNesting) {
                        if (nestingMode == ANGLE) {
                            // generics heuristic failed
                            nestingMode=BRACE;
                            nestingLevel= 1;
                        }
                        if (nestingMode == BRACE || nestingMode == NONE) {
                            nestingMode= BRACE;
                            nestingLevel++;
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case '}':
                    if (considerNesting) {
                        if (nestingMode == 0) {
                            return offset-1;
                        }
                        if (nestingMode == BRACE) {
                            if (--nestingLevel == 0) {
                                nestingMode= NONE;
                            }
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case '<':
                    if (considerNesting) {
                        if (nestingMode == ANGLE || nestingMode == NONE /*&& checkGenericsHeuristic(document, offset - 1, start - 1)*/) {
                            nestingMode= ANGLE;
                            nestingLevel++;
                        }
                        break;
                    }
                    //$FALL-THROUGH$
                case '>':
                    if (!lastWasEquals) {
                        if (nestingMode == 0) {
                            return offset-1;
                        }
                        if (considerNesting) {
                            if (nestingMode == ANGLE) {
                                if (--nestingLevel == 0) {
                                    nestingMode= NONE;
                                }
                            }
                            break;
                        }
                    }
                    //$FALL-THROUGH$
                default:
                    if (nestingLevel == 0) {
                        if (increments.indexOf(curr) >= 0) {
                            ++ charCount;
                        }
                        if (decrements.indexOf(curr) >= 0) {
                            -- charCount;
                        }
                    }
            }
            lastWasEquals = curr=='=';
        }

        return -1;
    }
    
    private static int[] computeCommaPositions(String code) {
        final int length= code.length();
        int pos = 0;
        int angleLevel = 0;
        List<Integer> positions= new ArrayList<Integer>();
        positions.add(new Integer(-1));
        char prev = ' ';
        while (pos < length && pos != -1) {
            char ch = code.charAt(pos);
            switch (ch) {
                case ',':
                case ';':
                    if (angleLevel == 0) {
                        positions.add(new Integer(pos));
                    }
                    break;
                case '<':
                case '(':
                case '{':
                case '[':
                    angleLevel++;
                    break;
                case '>':
                    if (prev=='=') break;
                case ')':
                case '}':
                case ']':
                    angleLevel--;
                    break;
//                case '[':
//                    pos= code.indexOf(']', pos);
//                    break;
                default:
                    break;
            }
            if (pos != -1) {
                pos++;
            }
        }
        positions.add(new Integer(length));

        int[] fields= new int[positions.size()];
        for (int i= 0; i < fields.length; i++) {
            fields[i]= positions.get(i).intValue();
        }
        return fields;
    }

    private static int getCommentEnd(IDocument d, int pos, int end) 
            throws BadLocationException {
        while (pos < end) {
            char curr= d.getChar(pos);
            pos++;
            if (curr == '*') {
                if (pos < end && d.getChar(pos) == '/') {
                    return pos + 1;
                }
            }
        }
        return end;
    }

    private static int getStringEnd(IDocument d, int pos, int end, char ch) 
            throws BadLocationException {
        while (pos < end) {
            char curr= d.getChar(pos);
            pos++;
            if (curr == '\\') {
                // ignore escaped characters
                pos++;
            }
            else if (curr == ch) {
                return pos;
            }
        }
        return end;
    }
    
}